2019/08/06

Recent entries from same category

  1. Go 言語プログラミングエッセンスという本を書きました。
  2. errors.Join が入った。
  3. unsafe.StringData、unsafe.String、unsafe.SliceData が入った。
  4. Re: Go言語で画像ファイルか確認してみる
  5. net/url に JoinPath が入った。

Go の標準パッケージのコードには稀に意図的にそうなっているのか分からない、速度に寄与するのかどうか確かめたくなる物が入っている事があります。

先日も見つけました。まだマージされてないですが、os.Mkdir に NUL という文字列を渡した時にファーストパスでエラーを返す変更です。

186139: os: return an error when the argument of Mkdir on Windows is os.DevNull
https://go-review.googlesource.com/c/go/+/186139/5/src/os/file.go#563

ここで出てくる以下のコード。

func isDevNull(name stringbool 
    if len(name) != 3 {
        return false
    }
    if name[0]|0x20 != 'n' {
        return false
    }
    if name[1]|0x20 != 'u' {
        return false
    }
    if name[2]|0x20 != 'l' {
        return false
    }
    return true
}

3文字の NUL を大文字小文字無視で比較しています。ビットマスクで大文字小文字を同一視しつつ、1つでも条件にマッチしない物があれば即 false を返すという古き良きC言語的なハックが使われています。

さて、このコードは本当に速度に寄与するのでしょうか?

package lowercase

import (
    "strings"
    "testing"
)

func isDevNull1(name stringbool {
    if len(name) != 3 {
        return false
    }
    if name[0]|0x20 != 'n' {
        return false
    }
    if name[1]|0x20 != 'u' {
        return false
    }
    if name[2]|0x20 != 'l' {
        return false
    }
    return true
}

func isDevNull2(name stringbool {
    if len(name) != 3 {
        return false
    }
    if name[0!= 'n' && name[0!= 'N' {
        return false
    }
    if name[1!= 'u' && name[1!= 'U' {
        return false
    }
    if name[2!= 'l' && name[2!= 'L' {
        return false
    }
    return true
}

func isDevNull3(name stringbool {
    return strings.ToLower(name) == "nul"
}

var tests = []struct {
    in     string
    result bool
}{
    {"nul"true},
    {"Nul"true},
    {"nui"false},
    {"lun"false},
    {"nulllllllllllllll"false},
    {"nuuuuuuuul"false},
    {strings.Repeat("N"3000), false},
}

func test(b *testing.B, f func(string) bool) {
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        for _, test := range tests {
            if got := f(test.in); got != test.result {
                b.Fatalf("want %v but got %v for %v", test.result, got, test.in)
            }
        }
    }
}

func BenchmarkS1(b *testing.B) {
    test(b, isDevNull1)
}

func BenchmarkS2(b *testing.B) {
    test(b, isDevNull2)
}

func BenchmarkS3(b *testing.B) {
    test(b, isDevNull3)
}

isDevNull1 が今回のコード、isDevNull2 が改良前のコード、isDevNull3 が入力文字を予め小文字に変換し比較するコードです。ベンチマークの実行結果は以下の通り。

goos: windows
goarch: amd64
pkg: github.com/mattn/go-sandbox/b3
BenchmarkS1-4           47997120                25.5 ns/op
BenchmarkS2-4           41377027                28.5 ns/op
BenchmarkS3-4             136354              8786 ns/op
PASS
ok      github.com/mattn/go-sandbox/b3  3.840s

Windows 64bit Core i7 16GB の結果です。今回改良されるコードが微妙ながら速度に寄与している事が分かりました。逆に言えばこの程度しか寄与していないので、可読性を優先する様なコードであれば isDevNull2 で充分かなとも思います。

Posted at by